package com.zzyl.nursing.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.zzyl.common.constant.CacheConstants;
import com.zzyl.nursing.domain.pojo.AlertData;
import com.zzyl.nursing.domain.pojo.AlertRule;
import com.zzyl.nursing.domain.pojo.DeviceData;
import com.zzyl.nursing.domain.vo.AlertNotifyVo;
import com.zzyl.nursing.mapper.DeviceMapper;
import com.zzyl.nursing.service.IAlertDataService;
import com.zzyl.nursing.service.IAlertHandleService;
import com.zzyl.nursing.service.IAlertRuleService;
import com.zzyl.nursing.ws.WebSocketServer;
import com.zzyl.system.mapper.SysUserRoleMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author robin
 * @date 25.6.3 11:46
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class AlertHandleServiceImpl implements IAlertHandleService {
    private final IAlertRuleService alertRuleService;
    private final StringRedisTemplate stringRedisTemplate;
    private final DeviceMapper deviceMapper;
    private final SysUserRoleMapper userRoleMapper;
    private final IAlertDataService alertDataService;
    private final WebSocketServer webSocketServer;
    
    /**
     * 一、获取所有设备的所有属性数据
     * 1.校验：当没有规则时候不需要校验
     * 2.拿到redis中所有设备数据
     * 3.数据做一下转换得到所有属性数据：List<DeviceData>
     * 4.对所有属性数据进行循环遍历进行数据比对：findAllAlertRule(deviceData)
     */
    @Override
    public void alertFilter() {
        // 1.校验：当没有规则时候不需要校验
        List<AlertRule> alertRules=alertRuleService.lambdaQuery()
                                                   .eq(AlertRule::getStatus,1)
                                                   .list();
        if (CollUtil.isEmpty(alertRules)) {
            return;
        }
        // 2.拿到redis中所有设备数据
        List<Object> allDeviceDatasJsonList=stringRedisTemplate.opsForHash()
                                                               .values(CacheConstants.IOT_DEVICE_LAST_DATA);
        if (CollUtil.isEmpty(allDeviceDatasJsonList)) {
            return;
        }
        // 3.数据做一下转换得到所有属性数据：List<DeviceData>
        List<DeviceData> deviceDataList=new ArrayList<>();
        allDeviceDatasJsonList.forEach(deviceDatasJson -> {
            deviceDataList.addAll(JSONUtil.toList(deviceDatasJson.toString(),DeviceData.class));
        });
        // 4.对所有属性数据进行循环遍历进行数据比对：findAllAlertRule(deviceData)
        deviceDataList.forEach(this::findAllAlertRule);
    }
    
    /**
     * 二、获取规则表中所有规则
     * 1.判断当前传递过来的属性数据是否在一分钟内
     * 2.从表中获取状态为启用所有规则：List<AlertRule>进行判断
     * 3.对所有报警规则进行遍历：拿每一个规则和这里传递的属性数据进行比对compareHandle(alertRule,deviceData)
     */
    private void findAllAlertRule(DeviceData deviceData) {
        // 1.判断当前传递过来的属性数据是否在一分钟内
        LocalDateTime alarmTime=deviceData.getAlarmTime();
        LocalDateTime localTime=LocalDateTime.now();
        long between=LocalDateTimeUtil.between(alarmTime,localTime,ChronoUnit.SECONDS);
        // 如果差值在1分钟外，那么该属性数据则不需要比对;因为之前已经处理过了，服务内部是按照1分钟来定时处理的
        // if (between > 60) {
        //     return;
        // }
        // 2.从表中获取状态为启用所有规则：List<AlertRule>进行判断
        List<AlertRule> currentDeviceAlertRules=alertRuleService.lambdaQuery()
                                                                .eq(AlertRule::getStatus,1)
                                                                .eq(AlertRule::getProductKey,deviceData.getProductKey())
                                                                .eq(AlertRule::getIotId,deviceData.getIotId())
                                                                .eq(AlertRule::getFunctionId,deviceData.getFunctionId())
                                                                .list();
        List<AlertRule> allDeviceAlertRules=alertRuleService.lambdaQuery()
                                                            .eq(AlertRule::getStatus,1)
                                                            .eq(AlertRule::getProductKey,deviceData.getProductKey())
                                                            .eq(AlertRule::getIotId,"-1")
                                                            .eq(AlertRule::getFunctionId,deviceData.getFunctionId())
                                                            .list();
        CollUtil.addAllIfNotContains(allDeviceAlertRules,currentDeviceAlertRules);
        // 3.对所有报警规则进行遍历：拿每一个规则和这里传递的属性数据进行比对compareHandle(alertRule,deviceData)
        allDeviceAlertRules.forEach(deviceAlertRule -> {
            compareHandle(deviceAlertRule,deviceData);
        });
        
    }
    
    /**
     * 三、拿每一个规则和传递过来的属性数据进行比对
     * 1.【判断】判断：属性数据中存在alarmTime是否在规则中定义有效时间
     * 2.【检查异常】比对：属性数据dataValue和规则中阈值以及比较符进行比对；
     * 3.【redis中沉默周期】如果是异常数据：从redis中查询是否有报警过（有报警就不需要处理，没有报警需要进行下面其他操作）
     * 4.【redis中计数】如果是异常数据且没有报警过：从redis中查询异常数据次数和规则中定义持续周期进行比较看是否构成报警
     * 5.【找通知人】一旦确定报警需要找到报警通知人员并批量插入报警数据  saveBatchAlertData(deviceData,alertRule)
     * 6.【短信或者站内消息通知】websocket进行通知对应报警消息
     */
    private void compareHandle(AlertRule alertRule,DeviceData deviceData) {
        log.info("拿每一个规则和每一个属性数据比对开始.....");
        // 1.【判断】判断：属性数据中存在alarmTime是否在规则中定义有效时间
        LocalTime alarmTime=deviceData.getAlarmTime()
                                      .toLocalTime();
        String alertEffectivePeriod=alertRule.getAlertEffectivePeriod();
        LocalTime startLocalTime=LocalDateTimeUtil.parse(alertEffectivePeriod.split("~")[0],DatePattern.NORM_TIME_PATTERN)
                                                  .toLocalTime();
        LocalTime endLocalTime=LocalDateTimeUtil.parse(alertEffectivePeriod.split("~")[1],DatePattern.NORM_TIME_PATTERN)
                                                .toLocalTime();
        if (alarmTime.isBefore(startLocalTime) || alarmTime.isAfter(endLocalTime)) {
            return;
        }
        // 2.【检查异常】比对：属性数据dataValue和规则中阈值以及比较符进行比对；
        float functionIdDataValue=Float.parseFloat(deviceData.getDataValue());
        String operator=alertRule.getOperator();
        float functionIdRuleValue=alertRule.getValue();
        // 举出一个例子：functionIdDataValue(88) >= functionIdRuleValue(55) &&  operator.equal(">=")
        int compare=Float.compare(functionIdDataValue,functionIdRuleValue);
        boolean isAbnormal=compare >= 0 && ">=".equals(operator) || compare < 0 && "<".equals(operator);
        
        String abnormalDeviceDataCountKey=CacheConstants.IOT_COUNT_ALERT + deviceData.getIotId() + ":" + alertRule.getId();
        // 如果不是异常数据则不需要处理
        if (BooleanUtil.isFalse(isAbnormal)) {
            // 删除redis中异常数据统计个数
            stringRedisTemplate.delete(abnormalDeviceDataCountKey);
            return;
        }
        // 3.【redis中沉默周期】如果是异常数据：从redis中查询是否有报警过（有报警就不需要处理，没有报警需要进行下面其他操作）
        String abnormalSilentAlertKey=CacheConstants.IOT_SILENT_ALERT + deviceData.getIotId() + ":" + alertRule.getId();
        if (BooleanUtil.isTrue(stringRedisTemplate.hasKey(abnormalSilentAlertKey))) {
            log.info("该设备属性已经报警过，不需要重复报警，{}分钟之后会重启检测报警",alertRule.getAlertSilentPeriod());
            return;
        }
        
        // 4.【redis中计数】如果是异常数据且没有报警过：从redis中查询异常数据次数和规则中定义持续周期进行比较看是否构成报警
        String abnormalDeviceDataAlertCountStr=stringRedisTemplate.opsForValue()
                                                                  .get(abnormalDeviceDataCountKey);
        int abnormalDeviceDataAlertCount=StrUtil.isNotBlank(abnormalDeviceDataAlertCountStr) ? Integer.parseInt(abnormalDeviceDataAlertCountStr) : 0;
        Integer duration=alertRule.getDuration();
        if (abnormalDeviceDataAlertCount + 1 < duration) {
            // 此时异常数据还无法构成报警通知，需要去redis计算1次
            stringRedisTemplate.opsForValue()
                               .increment(abnormalDeviceDataCountKey);
            return;
        }
        log.info("=====异常数据已经连续{}次检测是异常，此时开始触发报警===>",duration);
        // 删除redis中异常数据统计个数
        stringRedisTemplate.delete(abnormalDeviceDataCountKey);
        stringRedisTemplate.opsForValue()
                           .set(abnormalSilentAlertKey,"true",alertRule.getAlertSilentPeriod(),TimeUnit.MINUTES);
        // 5.【找通知人】一旦确定报警需要找到报警通知人员并批量插入报警数据  saveBatchAlertData(deviceData,alertRule)
        List<Long> notifyUserIdList=saveBatchAlertData(deviceData,alertRule);
        // 6.【短信或者站内消息通知】websocket进行通知对应报警消息
        sendMsgNotify(alertRule,deviceData,notifyUserIdList);
    }
    
    /**
     * 四、找报警通知人员批量保存报警数据
     * 1.要找到通知人员
     * 2.有多少个通知人员就构建多个报警数据然后批量保存到报警数据表中
     */
    private List<Long> saveBatchAlertData(DeviceData deviceData,AlertRule alertRule) {
        // 1.要找到通知人员
        Integer physicalLocationType=deviceData.getPhysicalLocationType();
        List<Long> notifyUserIdList=new ArrayList<>();
        switch (physicalLocationType) {
            // 如果是随身设备：设备bindLocation(绑定了老人的id)-->老人（elder）-->护理员(nursing_elder)
            case -1:
                notifyUserIdList=deviceMapper.selectNursingIdsByIotIdWithElder(deviceData.getIotId());
                break;
            // 如果是房间设备：直接根据角色名称（维修工）找用户
            case 1:
                notifyUserIdList=userRoleMapper.selectUserIdByRoleName("维修工");
                break;
            // 如果是床上设备：设备bindLocation(绑定了床的id)-->床（床的id）--->老人（elder）-->护理员(nursing_elder)
            case 2:
                notifyUserIdList=deviceMapper.selectNursingIdsByIotIdWithBed(deviceData.getIotId());
                break;
        }
        // 顺便抄送给领导通知
        notifyUserIdList.addAll(userRoleMapper.selectUserIdByRoleName("超级管理员"));
        // 2.有多少个通知人员就构建多个报警数据然后批量保存到报警数据表中
        List<AlertData> alertDataList=new ArrayList<>();
        notifyUserIdList.forEach(notifyUserId -> {
            // 填充设备属性相关字段
            AlertData alertData=BeanUtil.toBean(deviceData,AlertData.class,CopyOptions.create()
                                                                                      .setIgnoreProperties("id","createTime","createBy"));
            // 填充其他字段
            alertData.setAlertRuleId(alertRule.getId());
            alertData.setType(alertRule.getAlertDataType());
            // 类似：log.info("xxx:{}",yy) 字符串模板
            String alertReason=StrUtil.format("设备{}出现异常：{}{}{}，连续出现{}检测异常，请您及时关注并处理！",deviceData.getDeviceName(),alertRule.getFunctionId(),alertRule.getOperator(),
                                              alertRule.getValue(),alertRule.getDuration());
            alertData.setStatus(0);
            alertData.setAlertReason(alertReason);
            alertData.setUserId(notifyUserId);
            alertDataList.add(alertData);
        });
        alertDataService.saveBatch(alertDataList);
        return notifyUserIdList;
    }
    
    /**
     * 五、短信或者站内通知
     * 1.构建通知消息模板
     * 2.通过websocket来实现站内通知
     */
    private void sendMsgNotify(AlertRule alertRule,DeviceData deviceData,List<Long> userIdList) {
        // 1.构建通知消息模板
        AlertNotifyVo alertNotifyVo=new AlertNotifyVo();
        BeanUtil.copyProperties(deviceData,alertNotifyVo);
        alertNotifyVo.setFunctionName(alertRule.getFunctionName());
        alertNotifyVo.setAlertDataType(alertRule.getAlertDataType());
        alertNotifyVo.setVoiceNotifyStatus(0);
        alertNotifyVo.setNotifyType(1);
        // 2.通过websocket来实现站内通知
        webSocketServer.sendMessageToConsumer(alertNotifyVo,userIdList);
    }
}
